/*
 * Copyright (c) 2017 Baidu, Inc. All Rights Reserve. Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language governing permissions and limitations under the
 * License.
 */
package org.jflame.uid.snowflake;

import org.jflame.uid.IDGenerateException;
import org.jflame.uid.IDGenerator;
import org.jflame.uid.utils.NetUtils;
import org.jflame.uid.worker.WorkerIdAssigner;

import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Represents an implementation of {@link IDGenerator} The unique id has 64bits (long), default allocated as blow:<br>
 * sign: The highest bit is 0<br>
 * delta seconds: The next 28 bits, represents delta seconds since a customer epoch(2016-05-20 00:00:00.000). Supports
 * about 8.7 years until to 2024-11-20 21:24:16<br>
 * worker id: The next 22 bits, represents the worker's id which assigns based on database, max id is about 420W<br>
 * sequence: The next 13 bits, represents a sequence within the same second, max for 8192/s<br>
 * <br>
 * The {@link DefaultUidGenerator#parseUID(long)} is a tool method to parse the bits
 *
 * <pre>
 * {@code
 * +------+----------------------+----------------+-----------+
 * | sign |     delta seconds    | worker node id | sequence  |
 * +------+----------------------+----------------+-----------+
 *   1bit          28bits              22bits         13bits
 * }
 * </pre>
 *
 * You can also specified the bits by Spring property setting. timeBits: default as 28<br>
 * workerBits: default as 22<br>
 * seqBits: default as 13<br>
 * epochStr: Epoch date string format 'yyyy-MM-dd'. Default as '2021-05-20'<br>
 * <p>
 * <b>Note that:</b> The total bits must be 64 -1
 *
 * @author yutianbao
 */
public class DefaultUidGenerator implements IDGenerator<Long> {

    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultUidGenerator.class);

    /** Bits allocate */
    protected int timeBits = 31;
    protected int workerBits = 23;
    protected int seqBits = 9;

    /** Customer epoch, unit as second. For example 2021-05-20 (ms: 1621440000L) */
    protected String epochStr = "2021-05-20";
    protected long epochSeconds = 1621440000L;

    /** Stable fields after spring bean initializing */
    protected BitsAllocator bitsAllocator;
    protected long workerId;

    /** Volatile fields caused by nextId() */
    protected long sequence = 0L;
    protected long lastSecond = -1L;
    // 时间最大偏移量,即时间回滚在该范围内会等待再生成而不是直接抛异常,并发不多的系统可设置
    private long maxTimeOffset = 10;

    protected WorkerIdAssigner workerIdAssigner;

    public DefaultUidGenerator() {
    }

    public DefaultUidGenerator(WorkerIdAssigner workerIdAssigner) {
        this.workerIdAssigner = workerIdAssigner;
    }

    public DefaultUidGenerator(WorkerIdAssigner workerIdAssigner, int timeBits, int workerBits, int seqBits) {
        setTimeBits(timeBits);
        setWorkerBits(workerBits);
        setSeqBits(seqBits);
        this.workerIdAssigner = workerIdAssigner;
    }

    @Override
    public void init() {
        // initialize bits allocator
        bitsAllocator = new BitsAllocator(timeBits, workerBits, seqBits);

        // initialize worker id
        if (workerIdAssigner == null) {
            // 用本地网卡和JVM进程号生成个workerid
            workerId = createWorkerId(bitsAllocator.getMaxWorkerId());
        } else {
            workerId = workerIdAssigner.assignWorkerId();
        }
        if (workerId > bitsAllocator.getMaxWorkerId()) {
            throw new RuntimeException("Worker id " + workerId + " exceeds the max " + bitsAllocator.getMaxWorkerId());
        }

        LOGGER.info("Initialized bits(1, {}, {}, {}) for workerID:{}", timeBits, workerBits, seqBits, workerId);
    }

    @Override
    public Long nextId(String key) throws IDGenerateException {
        return nextId();
    }

    public String parseUID(long uid) {
        long totalBits = BitsAllocator.TOTAL_BITS;
        long signBits = bitsAllocator.getSignBits();
        long timestampBits = bitsAllocator.getTimestampBits();
        long workerIdBits = bitsAllocator.getWorkerIdBits();
        long sequenceBits = bitsAllocator.getSequenceBits();

        // parse UID
        long sequence = (uid << (totalBits - sequenceBits)) >>> (totalBits - sequenceBits);
        long workerId = (uid << (timestampBits + signBits)) >>> (totalBits - workerIdBits);
        long deltaSeconds = uid >>> (workerIdBits + sequenceBits);

        Date thatTime = new Date(TimeUnit.SECONDS.toMillis(epochSeconds + deltaSeconds));
        // String thatTimeStr = DateHelper.formatLong(thatTime);

        // format as string
        return String.format("{\"UID\":\"%d\",\"timestamp\":\"%s\",\"workerId\":\"%d\",\"sequence\":\"%d\"}", uid,
                thatTime, workerId, sequence);
    }

    /**
     * Get UID
     *
     * @return UID
     * @throws UidGenerateException in the case: Clock moved backwards; Exceeds the max timestamp
     */
    protected synchronized long nextId() {
        long currentSecond = getCurrentSecond();

        // Clock moved backwards, refuse to generate uid
        if (currentSecond < lastSecond) {
            long offset = lastSecond - currentSecond;
            if (offset <= maxTimeOffset) {
                try {
                    wait(offset);
                } catch (InterruptedException e) {

                }
                currentSecond = getCurrentSecond();
                if (currentSecond < lastSecond) {
                    throw new IDGenerateException("Clock moved backwards. Refusing for %d seconds", offset);
                }
            } else {
                throw new IDGenerateException("Clock moved backwards. Refusing for %d seconds", offset);
            }
        }

        // At the same second, increase sequence
        if (currentSecond == lastSecond) {
            sequence = (sequence + 1) & bitsAllocator.getMaxSequence();
            // Exceed the max sequence, we wait the next second to generate uid
            if (sequence == 0) {
                currentSecond = getNextSecond(lastSecond);
            }

            // At the different second, sequence restart from zero
        } else {
            sequence = randomInitSequence();
        }

        lastSecond = currentSecond;

        // Allocate bits for UID
        return bitsAllocator.allocate(currentSecond - epochSeconds, workerId, sequence);
    }

    /**
     * 初始值使用0-9随机，避免最后一位为0的数过多， 有利取模分表情况
     * 
     * @return
     */
    private long randomInitSequence() {
        return ThreadLocalRandom.current()
                .nextInt(9);
    }

    /**
     * Get next millisecond
     */
    private long getNextSecond(long lastTimestamp) {
        long timestamp = getCurrentSecond();
        while (timestamp <= lastTimestamp) {
            timestamp = getCurrentSecond();
        }

        return timestamp;
    }

    /**
     * Get current second
     */
    private long getCurrentSecond() {
        long currentSecond = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
        if (currentSecond - epochSeconds > bitsAllocator.getMaxDeltaSeconds()) {
            throw new IDGenerateException("Timestamp bits is exhausted. Refusing UID generate. Now: " + currentSecond);
        }

        return currentSecond;
    }

    /**
     * Setters for spring property
     */
    public void setWorkerIdAssigner(WorkerIdAssigner workerIdAssigner) {
        this.workerIdAssigner = workerIdAssigner;
    }

    public void setTimeBits(int timeBits) {
        if (timeBits > 0) {
            this.timeBits = timeBits;
        }
    }

    public void setWorkerBits(int workerBits) {
        if (workerBits > 0) {
            this.workerBits = workerBits;
        }
    }

    public void setSeqBits(int seqBits) {
        if (seqBits > 0) {
            this.seqBits = seqBits;
        }
    }

    public void setEpochStr(String epochStr) {
        if (StringUtils.isNotBlank(epochStr)) {
            this.epochStr = epochStr;
            this.epochSeconds = toEpochSecond(LocalDate.parse(epochStr));
        }
    }

    public void setEpoch(LocalDate epoch) {
        if (epoch != null) {
            this.epochStr = epoch.toString();
            this.epochSeconds = toEpochSecond(epoch);
        }
    }

    static long toEpochSecond(LocalDate ld) {
        return ld.atStartOfDay(ZoneId.systemDefault())
                .toEpochSecond();
    }

    public void setMaxTimeOffset(long maxTimeOffset) {
        this.maxTimeOffset = maxTimeOffset;
    }

    /**
     * <p>
     * 获取 maxWorkerId
     * </p>
     */
    long createWorkerId(long maxWorkerId) {
        StringBuilder mpid = new StringBuilder();
        mpid.append(getDatacenterId());
        String name = ManagementFactory.getRuntimeMXBean()
                .getName();
        if (StringUtils.isNotEmpty(name)) {
            /*
             * GET jvmPid
             */
            mpid.append(name.split("@")[0]);
        }
        /*
         * MAC + PID 的 hashcode 获取16个低位
         */
        return (mpid.toString()
                .hashCode() & 0xffff) % (maxWorkerId + 1);
    }

    /**
     * <p>
     * 数据标识id部分
     * </p>
     */
    long getDatacenterId() {
        long id = 0L;
        try {
            InetAddress ip = NetUtils.getLocalInetAddress();
            NetworkInterface network = NetworkInterface.getByInetAddress(ip);
            if (network == null) {
                id = 1L;
            } else {
                byte[] mac = network.getHardwareAddress();
                if (null != mac) {
                    id = ((0x000000FF & (long) mac[mac.length - 1])
                            | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
                    id = id % 255;
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("获取数据中心标识异常");
        }
        return id;
    }

}
